package com.app.core.view;

/*
 * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under the License.
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.KeyEventCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Interpolator;
import android.widget.Scroller;

/**
 * Layout manager that allows the user to flip left and right through pages of data. You supply an implementation of a
 * {@link android.support.v4.view.PagerAdapter} to generate the pages that the view shows.
 * <p>
 * Note this class is currently under early design and development. The API will likely change in later updates of the compatibility
 * library, requiring changes to the source code of apps when they are compiled against the newer version.
 * </p>
 */
public class LazyViewPager extends ViewGroup {

    private static final String TAG="LazyViewPager";

    private static final boolean DEBUG=false;

    private static final boolean USE_CACHE=false;

    private static final int DEFAULT_OFFSCREEN_PAGES=0;// 默认的加载页面,ViewPager是1个,所以会加载两个Fragment

    private static final int MAX_SETTLE_DURATION=600; // ms

    static class ItemInfo {

        Object object;

        int position;

        boolean scrolling;
    }

    private static final Comparator<ItemInfo> COMPARATOR=new Comparator<ItemInfo>() {

        @Override
        public int compare(ItemInfo lhs, ItemInfo rhs) {
            return lhs.position - rhs.position;
        }
    };

    private static final Interpolator sInterpolator=new Interpolator() {

        public float getInterpolation(float t) {
            // _o(t) = t * t * ((tension + 1) * t + tension)
            // o(t) = _o(t - 1) + 1
            t-=1.0f;
            return t * t * t + 1.0f;
        }
    };

    private final ArrayList<ItemInfo> mItems=new ArrayList<ItemInfo>();

    private PagerAdapter mAdapter;

    private int mCurItem; // Index of currently displayed page.

    private int mRestoredCurItem=-1;

    private Parcelable mRestoredAdapterState=null;

    private ClassLoader mRestoredClassLoader=null;

    private Scroller mScroller;

    private PagerObserver mObserver;

    private int mPageMargin;

    private Drawable mMarginDrawable;

    private int mChildWidthMeasureSpec;

    private int mChildHeightMeasureSpec;

    private boolean mInLayout;

    private boolean mScrollingCacheEnabled;

    private boolean mPopulatePending;

    private boolean mScrolling;

    private int mOffscreenPageLimit=DEFAULT_OFFSCREEN_PAGES;

    private boolean mIsBeingDragged;

    private boolean mIsUnableToDrag;

    private int mTouchSlop;

    private float mInitialMotionX;

    /**
     * Position of the last motion event.
     */
    private float mLastMotionX;

    private float mLastMotionY;

    /**
     * ID of the active pointer. This is used to retain consistency during drags/flings if multiple pointers are used.
     */
    private int mActivePointerId=INVALID_POINTER;

    /**
     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
     */
    private static final int INVALID_POINTER=-1;

    /**
     * Determines speed during touch scrolling
     */
    private VelocityTracker mVelocityTracker;

    private int mMinimumVelocity;

    private int mMaximumVelocity;

    private float mBaseLineFlingVelocity;

    private float mFlingVelocityInfluence;

    private boolean mFakeDragging;

    private long mFakeDragBeginTime;

    private EdgeEffectCompat mLeftEdge;

    private EdgeEffectCompat mRightEdge;

    private boolean mFirstLayout=true;

    private OnPageChangeListener mOnPageChangeListener;

    /**
     * Indicates that the pager is in an idle, settled state. The current page is fully in view and no animation is in progress.
     */
    public static final int SCROLL_STATE_IDLE=0;

    /**
     * Indicates that the pager is currently being dragged by the user.
     */
    public static final int SCROLL_STATE_DRAGGING=1;

    /**
     * Indicates that the pager is in the process of settling to a final position.
     */
    public static final int SCROLL_STATE_SETTLING=2;

    private int mScrollState=SCROLL_STATE_IDLE;

    /**
     * Callback interface for responding to changing state of the selected page.
     */
    public interface OnPageChangeListener {

        /**
         * This method will be invoked when the current page is scrolled, either as part of a programmatically initiated smooth
         * scroll or a user initiated touch scroll.
         * @param position Position index of the first page currently being displayed. Page position+1 will be visible if
         *        positionOffset is nonzero.
         * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
         * @param positionOffsetPixels Value in pixels indicating the offset from position.
         */
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

        /**
         * This method will be invoked when a new page becomes selected. Animation is not necessarily complete.
         * @param position Position index of the new selected page.
         */
        public void onPageSelected(int position);

        /**
         * Called when the scroll state changes. Useful for discovering when the user begins dragging, when the pager is
         * automatically settling to the current page, or when it is fully stopped/idle.
         * @param state The new scroll state.
         * @see android.support.v4.view.ViewPager#SCROLL_STATE_IDLE
         * @see android.support.v4.view.ViewPager#SCROLL_STATE_DRAGGING
         * @see android.support.v4.view.ViewPager#SCROLL_STATE_SETTLING
         */
        public void onPageScrollStateChanged(int state);
    }

    /**
     * Simple implementation of the {@link com.bxg.news.view.LazyViewPager.OnPageChangeListener} interface with stub implementations
     * of each method. Extend this if you do not intend to override every method of
     * {@link com.bxg.news.view.LazyViewPager.OnPageChangeListener}.
     */
    public static class SimpleOnPageChangeListener implements OnPageChangeListener {

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            // This space for rent
        }

        @Override
        public void onPageSelected(int position) {
            // This space for rent
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            // This space for rent
        }
    }

    public LazyViewPager(Context context) {
        super(context);
        initViewPager();
    }

    public LazyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        initViewPager();
    }

    void initViewPager() {
        setWillNotDraw(false);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
        setFocusable(true);
        final Context context=getContext();
        mScroller=new Scroller(context, sInterpolator);
        final ViewConfiguration configuration=ViewConfiguration.get(context);
        mTouchSlop=ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        mMinimumVelocity=configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity=configuration.getScaledMaximumFlingVelocity();
        mLeftEdge=new EdgeEffectCompat(context);
        mRightEdge=new EdgeEffectCompat(context);

        float density=context.getResources().getDisplayMetrics().density;
        mBaseLineFlingVelocity=2500.0f * density;
        mFlingVelocityInfluence=0.4f;
    }

    private void setScrollState(int newState) {
        if(mScrollState == newState) {
            return;
        }

        mScrollState=newState;
        if(mOnPageChangeListener != null) {
            mOnPageChangeListener.onPageScrollStateChanged(newState);
        }
    }

    public void setAdapter(PagerAdapter adapter) {
        if(mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mObserver);
            mAdapter.startUpdate(this);
            for(int i=0; i < mItems.size(); i++) {
                final ItemInfo ii=mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
            mAdapter.finishUpdate(this);
            mItems.clear();
            removeAllViews();
            mCurItem=0;
            scrollTo(0, 0);
        }

        mAdapter=adapter;

        if(mAdapter != null) {
            if(mObserver == null) {
                mObserver=new PagerObserver();
            }
            mAdapter.registerDataSetObserver(mObserver);
            mPopulatePending=false;
            if(mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem=-1;
                mRestoredAdapterState=null;
                mRestoredClassLoader=null;
            } else {
                populate();
            }
        }
    }

    public PagerAdapter getAdapter() {
        return mAdapter;
    }

    /**
     * Set the currently selected page. If the ViewPager has already been through its first layout there will be a smooth animated
     * transition between the current item and the specified item.
     * @param item Item index to select
     */
    public void setCurrentItem(int item) {
        mPopulatePending=false;
        setCurrentItemInternal(item, !mFirstLayout, false);
    }

    /**
     * Set the currently selected page.
     * @param item Item index to select
     * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
     */
    public void setCurrentItem(int item, boolean smoothScroll) {
        mPopulatePending=false;
        setCurrentItemInternal(item, smoothScroll, false);
    }

    public int getCurrentItem() {
        return mCurItem;
    }

    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
        setCurrentItemInternal(item, smoothScroll, always, 0);
    }

    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        if(mAdapter == null || mAdapter.getCount() <= 0) {
            setScrollingCacheEnabled(false);
            return;
        }
        if(!always && mCurItem == item && mItems.size() != 0) {
            setScrollingCacheEnabled(false);
            return;
        }
        if(item < 0) {
            item=0;
        } else if(item >= mAdapter.getCount()) {
            item=mAdapter.getCount() - 1;
        }
        final int pageLimit=mOffscreenPageLimit;
        if(item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
            // We are doing a jump by more than one page. To avoid
            // glitches, we want to keep all current pages in the view
            // until the scroll ends.
            for(int i=0; i < mItems.size(); i++) {
                mItems.get(i).scrolling=true;
            }
        }

        final boolean dispatchSelected=mCurItem != item;
        mCurItem=item;
        populate();
        final int destX=(getWidth() + mPageMargin) * item;
        if(smoothScroll) {
            smoothScrollTo(destX, 0, velocity);
            if(dispatchSelected && mOnPageChangeListener != null) {
                mOnPageChangeListener.onPageSelected(item);
            }
        } else {
            if(dispatchSelected && mOnPageChangeListener != null) {
                mOnPageChangeListener.onPageSelected(item);
            }
            completeScroll();
            scrollTo(destX, 0);
        }
    }

    public void setOnPageChangeListener(OnPageChangeListener listener) {
        mOnPageChangeListener=listener;
    }

    /**
     * Returns the number of pages that will be retained to either side of the current page in the view hierarchy in an idle state.
     * Defaults to 1.
     * @return How many pages will be kept offscreen on either side
     * @see #setOffscreenPageLimit(int)
     */
    public int getOffscreenPageLimit() {
        return mOffscreenPageLimit;
    }

    /**
     * Set the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state.
     * Pages beyond this limit will be recreated from the adapter when needed.
     * <p>
     * This is offered as an optimization. If you know in advance the number of pages you will need to support or have lazy-loading
     * mechanisms in place on your pages, tweaking this setting can have benefits in perceived smoothness of paging animations and
     * interaction. If you have a small number of pages (3-4) that you can keep active all at once, less time will be spent in
     * layout for newly created view subtrees as the user pages back and forth.
     * </p>
     * <p>
     * You should keep this limit low, especially if your pages have complex layouts. This setting defaults to 1.
     * </p>
     * @param limit How many pages will be kept offscreen in an idle state.
     */
    public void setOffscreenPageLimit(int limit) {
        if(limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + DEFAULT_OFFSCREEN_PAGES);
            limit=DEFAULT_OFFSCREEN_PAGES;
        }
        if(limit != mOffscreenPageLimit) {
            mOffscreenPageLimit=limit;
            populate();
        }
    }

    /**
     * Set the margin between pages.
     * @param marginPixels Distance between adjacent pages in pixels
     * @see #getPageMargin()
     * @see #setPageMarginDrawable(android.graphics.drawable.Drawable)
     * @see #setPageMarginDrawable(int)
     */
    public void setPageMargin(int marginPixels) {
        final int oldMargin=mPageMargin;
        mPageMargin=marginPixels;

        final int width=getWidth();
        recomputeScrollPosition(width, width, marginPixels, oldMargin);

        requestLayout();
    }

    /**
     * Return the margin between pages.
     * @return The size of the margin in pixels
     */
    public int getPageMargin() {
        return mPageMargin;
    }

    /**
     * Set a drawable that will be used to fill the margin between pages.
     * @param d Drawable to display between pages
     */
    public void setPageMarginDrawable(Drawable d) {
        mMarginDrawable=d;
        if(d != null)
            refreshDrawableState();
        setWillNotDraw(d == null);
        invalidate();
    }

    /**
     * Set a drawable that will be used to fill the margin between pages.
     * @param resId Resource ID of a drawable to display between pages
     */
    public void setPageMarginDrawable(int resId) {
        setPageMarginDrawable(getContext().getResources().getDrawable(resId));
    }

    @Override
    protected boolean verifyDrawable(Drawable who) {
        return super.verifyDrawable(who) || who == mMarginDrawable;
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        final Drawable d=mMarginDrawable;
        if(d != null && d.isStateful()) {
            d.setState(getDrawableState());
        }
    }

    // We want the duration of the page snap animation to be influenced by the
    // distance that
    // the screen has to travel, however, we don't want this duration to be
    // effected in a
    // purely linear fashion. Instead, we use this method to moderate the effect
    // that the distance
    // of travel has on the overall snap duration.
    float distanceInfluenceForSnapDuration(float f) {
        f-=0.5f; // center the values about 0.
        f*=0.3f * Math.PI / 2.0f;
        return (float)Math.sin(f);
    }

    /**
     * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
     * @param x the number of pixels to scroll by on the X axis
     * @param y the number of pixels to scroll by on the Y axis
     */
    void smoothScrollTo(int x, int y) {
        smoothScrollTo(x, y, 0);
    }

    /**
     * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
     * @param x the number of pixels to scroll by on the X axis
     * @param y the number of pixels to scroll by on the Y axis
     * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
     */
    void smoothScrollTo(int x, int y, int velocity) {
        if(getChildCount() == 0) {
            // Nothing to do.
            setScrollingCacheEnabled(false);
            return;
        }
        int sx=getScrollX();
        int sy=getScrollY();
        int dx=x - sx;
        int dy=y - sy;
        if(dx == 0 && dy == 0) {
            completeScroll();
            setScrollState(SCROLL_STATE_IDLE);
            return;
        }

        setScrollingCacheEnabled(true);
        mScrolling=true;
        setScrollState(SCROLL_STATE_SETTLING);

        final float pageDelta=(float)Math.abs(dx) / (getWidth() + mPageMargin);
        int duration=(int)(pageDelta * 100);

        velocity=Math.abs(velocity);
        if(velocity > 0) {
            duration+=(duration / (velocity / mBaseLineFlingVelocity)) * mFlingVelocityInfluence;
        } else {
            duration+=100;
        }
        duration=Math.min(duration, MAX_SETTLE_DURATION);

        mScroller.startScroll(sx, sy, dx, dy, duration);
        invalidate();
    }

    void addNewItem(int position, int index) {
        ItemInfo ii=new ItemInfo();
        ii.position=position;
        ii.object=mAdapter.instantiateItem(this, position);
        if(index < 0) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
    }

    void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter
        // is non-null.

        boolean needPopulate=mItems.size() < 3 && mItems.size() < mAdapter.getCount();
        int newCurrItem=-1;

        for(int i=0; i < mItems.size(); i++) {
            final ItemInfo ii=mItems.get(i);
            final int newPos=mAdapter.getItemPosition(ii.object);

            if(newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if(newPos == PagerAdapter.POSITION_NONE) {
                mItems.remove(i);
                i--;
                mAdapter.destroyItem(this, ii.position, ii.object);
                needPopulate=true;

                if(mCurItem == ii.position) {
                    // Keep the current item in the valid range
                    newCurrItem=Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1));
                }
                continue;
            }

            if(ii.position != newPos) {
                if(ii.position == mCurItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem=newPos;
                }

                ii.position=newPos;
                needPopulate=true;
            }
        }

        Collections.sort(mItems, COMPARATOR);

        if(newCurrItem >= 0) {
            // TODO This currently causes a jump.
            setCurrentItemInternal(newCurrItem, false, true);
            needPopulate=true;
        }
        if(needPopulate) {
            populate();
            requestLayout();
        }
    }

    void populate() {
        if(mAdapter == null) {
            return;
        }

        // Bail now if we are waiting to populate. This is to hold off
        // on creating views from the time the user releases their finger to
        // fling to a new position until we have finished the scroll to
        // that position, avoiding glitches from happening at that point.
        if(mPopulatePending) {
            if(DEBUG)
                Log.i(TAG, "populate is pending, skipping for now...");
            return;
        }

        // Also, don't populate until we are attached to a window. This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if(getWindowToken() == null) {
            return;
        }

        mAdapter.startUpdate(this);

        final int pageLimit=mOffscreenPageLimit;
        final int startPos=Math.max(0, mCurItem - pageLimit);
        final int N=mAdapter.getCount();
        final int endPos=Math.min(N - 1, mCurItem + pageLimit);

        if(DEBUG)
            Log.v(TAG, "populating: startPos=" + startPos + " endPos=" + endPos);

        // Add and remove pages in the existing list.
        int lastPos=-1;
        for(int i=0; i < mItems.size(); i++) {
            ItemInfo ii=mItems.get(i);
            if((ii.position < startPos || ii.position > endPos) && !ii.scrolling) {
                if(DEBUG)
                    Log.i(TAG, "removing: " + ii.position + " @ " + i);
                mItems.remove(i);
                i--;
                mAdapter.destroyItem(this, ii.position, ii.object);
            } else if(lastPos < endPos && ii.position > startPos) {
                // The next item is outside of our range, but we have a gap
                // between it and the last item where we want to have a page
                // shown. Fill in the gap.
                lastPos++;
                if(lastPos < startPos) {
                    lastPos=startPos;
                }
                while(lastPos <= endPos && lastPos < ii.position) {
                    if(DEBUG)
                        Log.i(TAG, "inserting: " + lastPos + " @ " + i);
                    addNewItem(lastPos, i);
                    lastPos++;
                    i++;
                }
            }
            lastPos=ii.position;
        }

        // Add any new pages we need at the end.
        lastPos=mItems.size() > 0 ? mItems.get(mItems.size() - 1).position : -1;
        if(lastPos < endPos) {
            lastPos++;
            lastPos=lastPos > startPos ? lastPos : startPos;
            while(lastPos <= endPos) {
                if(DEBUG)
                    Log.i(TAG, "appending: " + lastPos);
                addNewItem(lastPos, -1);
                lastPos++;
            }
        }

        if(DEBUG) {
            Log.i(TAG, "Current page list:");
            for(int i=0; i < mItems.size(); i++) {
                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
            }
        }

        ItemInfo curItem=null;
        for(int i=0; i < mItems.size(); i++) {
            if(mItems.get(i).position == mCurItem) {
                curItem=mItems.get(i);
                break;
            }
        }
        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);

        mAdapter.finishUpdate(this);

        if(hasFocus()) {
            View currentFocused=findFocus();
            ItemInfo ii=currentFocused != null ? infoForAnyChild(currentFocused) : null;
            if(ii == null || ii.position != mCurItem) {
                for(int i=0; i < getChildCount(); i++) {
                    View child=getChildAt(i);
                    ii=infoForChild(child);
                    if(ii != null && ii.position == mCurItem) {
                        if(child.requestFocus(FOCUS_FORWARD)) {
                            break;
                        }
                    }
                }
            }
        }
    }

    public static class SavedState extends BaseSavedState {

        int position;

        Parcelable adapterState;

        ClassLoader loader;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(position);
            out.writeParcelable(adapterState, flags);
        }

        @Override
        public String toString() {
            return "FragmentPager.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " position=" + position + "}";
        }

        public static final Creator<SavedState> CREATOR=ParcelableCompat
            .newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {

                @Override
                public SavedState createFromParcel(Parcel in, ClassLoader loader) {
                    return new SavedState(in, loader);
                }

                @Override
                public SavedState[] newArray(int size) {
                    return new SavedState[size];
                }
            });

        SavedState(Parcel in, ClassLoader loader) {
            super(in);
            if(loader == null) {
                loader=getClass().getClassLoader();
            }
            position=in.readInt();
            adapterState=in.readParcelable(loader);
            this.loader=loader;
        }
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState=super.onSaveInstanceState();
        SavedState ss=new SavedState(superState);
        ss.position=mCurItem;
        if(mAdapter != null) {
            ss.adapterState=mAdapter.saveState();
        }
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if(!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState ss=(SavedState)state;
        super.onRestoreInstanceState(ss.getSuperState());

        if(mAdapter != null) {
            mAdapter.restoreState(ss.adapterState, ss.loader);
            setCurrentItemInternal(ss.position, false, true);
        } else {
            mRestoredCurItem=ss.position;
            mRestoredAdapterState=ss.adapterState;
            mRestoredClassLoader=ss.loader;
        }
    }

    @Override
    public void addView(View child, int index, LayoutParams params) {
        if(mInLayout) {
            addViewInLayout(child, index, params);
            child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
        } else {
            super.addView(child, index, params);
        }

        if(USE_CACHE) {
            if(child.getVisibility() != GONE) {
                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
            } else {
                child.setDrawingCacheEnabled(false);
            }
        }
    }

    ItemInfo infoForChild(View child) {
        for(int i=0; i < mItems.size(); i++) {
            ItemInfo ii=mItems.get(i);
            if(mAdapter.isViewFromObject(child, ii.object)) {
                return ii;
            }
        }
        return null;
    }

    ItemInfo infoForAnyChild(View child) {
        ViewParent parent;
        while((parent=child.getParent()) != this) {
            if(parent == null || !(parent instanceof View)) {
                return null;
            }
            child=(View)parent;
        }
        return infoForChild(child);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mFirstLayout=true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // For simple implementation, or internal size is always 0.
        // We depend on the container to specify the layout size of
        // our view. We can't really know what it is since we will be
        // adding and removing different arbitrary views and do not
        // want the layout to change as this happens.
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));

        // Children are just made to fill our space.
        mChildWidthMeasureSpec=
            MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
        mChildHeightMeasureSpec=
            MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);

        // Make sure we have created all fragments that we need to have shown.
        mInLayout=true;
        populate();
        mInLayout=false;

        // Make sure all children have been properly measured.
        final int size=getChildCount();
        for(int i=0; i < size; ++i) {
            final View child=getChildAt(i);
            if(child.getVisibility() != GONE) {
                if(DEBUG)
                    Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec);
                child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
            }
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // Make sure scroll position is set correctly.
        if(w != oldw) {
            recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
        }
    }

    private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
        final int widthWithMargin=width + margin;
        if(oldWidth > 0) {
            final int oldScrollPos=getScrollX();
            final int oldwwm=oldWidth + oldMargin;
            final int oldScrollItem=oldScrollPos / oldwwm;
            final float scrollOffset=(float)(oldScrollPos % oldwwm) / oldwwm;
            final int scrollPos=(int)((oldScrollItem + scrollOffset) * widthWithMargin);
            scrollTo(scrollPos, getScrollY());
            if(!mScroller.isFinished()) {
                // We now return to your regularly scheduled scroll, already in
                // progress.
                final int newDuration=mScroller.getDuration() - mScroller.timePassed();
                mScroller.startScroll(scrollPos, 0, mCurItem * widthWithMargin, 0, newDuration);
            }
        } else {
            int scrollPos=mCurItem * widthWithMargin;
            if(scrollPos != getScrollX()) {
                completeScroll();
                scrollTo(scrollPos, getScrollY());
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mInLayout=true;
        populate();
        mInLayout=false;

        final int count=getChildCount();
        final int width=r - l;

        for(int i=0; i < count; i++) {
            View child=getChildAt(i);
            ItemInfo ii;
            if(child.getVisibility() != GONE && (ii=infoForChild(child)) != null) {
                int loff=(width + mPageMargin) * ii.position;
                int childLeft=getPaddingLeft() + loff;
                int childTop=getPaddingTop();
                if(DEBUG)
                    Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + ":" + childLeft + "," + childTop + " "
                        + child.getMeasuredWidth() + "x" + child.getMeasuredHeight());
                child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
            }
        }
        mFirstLayout=false;
    }

    @Override
    public void computeScroll() {
        if(DEBUG)
            Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished());
        if(!mScroller.isFinished()) {
            if(mScroller.computeScrollOffset()) {
                if(DEBUG)
                    Log.i(TAG, "computeScroll: still scrolling");
                int oldX=getScrollX();
                int oldY=getScrollY();
                int x=mScroller.getCurrX();
                int y=mScroller.getCurrY();

                if(oldX != x || oldY != y) {
                    scrollTo(x, y);
                }

                if(mOnPageChangeListener != null) {
                    final int widthWithMargin=getWidth() + mPageMargin;
                    final int position=x / widthWithMargin;
                    final int offsetPixels=x % widthWithMargin;
                    final float offset=(float)offsetPixels / widthWithMargin;
                    mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
                }

                // Keep on drawing until the animation has finished.
                invalidate();
                return;
            }
        }

        // Done with scroll, clean up state.
        completeScroll();
    }

    private void completeScroll() {
        boolean needPopulate=mScrolling;
        if(needPopulate) {
            // Done with scroll, no longer want to cache view drawing.
            setScrollingCacheEnabled(false);
            mScroller.abortAnimation();
            int oldX=getScrollX();
            int oldY=getScrollY();
            int x=mScroller.getCurrX();
            int y=mScroller.getCurrY();
            if(oldX != x || oldY != y) {
                scrollTo(x, y);
            }
            setScrollState(SCROLL_STATE_IDLE);
        }
        mPopulatePending=false;
        mScrolling=false;
        for(int i=0; i < mItems.size(); i++) {
            ItemInfo ii=mItems.get(i);
            if(ii.scrolling) {
                needPopulate=true;
                ii.scrolling=false;
            }
        }
        if(needPopulate) {
            populate();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion. If we return true, onMotionEvent will be called and
         * we do the actual scrolling there.
         */

        final int action=ev.getAction() & MotionEventCompat.ACTION_MASK;

        // Always take care of the touch gesture being complete.
        if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the drag.
            if(DEBUG)
                Log.v(TAG, "Intercept done!");
            mIsBeingDragged=false;
            mIsUnableToDrag=false;
            mActivePointerId=INVALID_POINTER;
            return false;
        }

        // Nothing more to do here if we have decided whether or not we
        // are dragging.
        if(action != MotionEvent.ACTION_DOWN) {
            if(mIsBeingDragged) {
                if(DEBUG)
                    Log.v(TAG, "Intercept returning true!");
                return true;
            }
            if(mIsUnableToDrag) {
                if(DEBUG)
                    Log.v(TAG, "Intercept returning false!");
                return false;
            }
        }

        switch(action) {
            case MotionEvent.ACTION_MOVE: {
                /*
                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check whether the user has moved far
                 * enough from his original down touch.
                 */

                /*
                 * Locally do absolute value. mLastMotionY is set to the y value of the down event.
                 */
                final int activePointerId=mActivePointerId;
                if(activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on
                    // content.
                    break;
                }

                final int pointerIndex=MotionEventCompat.findPointerIndex(ev, activePointerId);
                final float x=MotionEventCompat.getX(ev, pointerIndex);
                final float dx=x - mLastMotionX;
                final float xDiff=Math.abs(dx);
                final float y=MotionEventCompat.getY(ev, pointerIndex);
                final float yDiff=Math.abs(y - mLastMotionY);
                final int scrollX=getScrollX();
                final boolean atEdge=
                    (dx > 0 && scrollX == 0)
                        || (dx < 0 && mAdapter != null && scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1);
                if(DEBUG)
                    Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

                if(canScroll(this, false, (int)dx, (int)x, (int)y)) {
                    // Nested view has scrollable area under this point. Let it be
                    // handled there.
                    mInitialMotionX=mLastMotionX=x;
                    mLastMotionY=y;
                    return false;
                }
                if(xDiff > mTouchSlop && xDiff > yDiff) {
                    if(DEBUG)
                        Log.v(TAG, "Starting drag!");
                    mIsBeingDragged=true;
                    setScrollState(SCROLL_STATE_DRAGGING);
                    mLastMotionX=x;
                    setScrollingCacheEnabled(true);
                } else {
                    if(yDiff > mTouchSlop) {
                        // The finger has moved enough in the vertical
                        // direction to be counted as a drag... abort
                        // any attempt to drag horizontally, to work correctly
                        // with children that have scrolling containers.
                        if(DEBUG)
                            Log.v(TAG, "Starting unable to drag!");
                        mIsUnableToDrag=true;
                    }
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                /*
                 * Remember location of down touch. ACTION_DOWN always refers to pointer index 0.
                 */
                mLastMotionX=mInitialMotionX=ev.getX();
                mLastMotionY=ev.getY();
                mActivePointerId=MotionEventCompat.getPointerId(ev, 0);

                if(mScrollState == SCROLL_STATE_SETTLING) {
                    // Let the user 'catch' the pager as it animates.
                    mIsBeingDragged=true;
                    mIsUnableToDrag=false;
                    setScrollState(SCROLL_STATE_DRAGGING);
                } else {
                    completeScroll();
                    mIsBeingDragged=false;
                    mIsUnableToDrag=false;
                }

                if(DEBUG)
                    Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + " mIsBeingDragged=" + mIsBeingDragged
                        + "mIsUnableToDrag=" + mIsUnableToDrag);
                break;
            }

            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }

        /*
         * The only time we want to intercept motion events is if we are in the drag mode.
         */
        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(mFakeDragging) {
            // A fake drag is in progress already, ignore this real one
            // but still eat the touch events.
            // (It is likely that the user is multi-touching the screen.)
            return true;
        }

        if(ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
            // Don't handle edge touches immediately -- they may actually belong
            // to one of our
            // descendants.
            return false;
        }

        if(mAdapter == null || mAdapter.getCount() == 0) {
            // Nothing to present or scroll; nothing to touch.
            return false;
        }

        if(mVelocityTracker == null) {
            mVelocityTracker=VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

        final int action=ev.getAction();
        boolean needsInvalidate=false;

        switch(action & MotionEventCompat.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                /*
                 * If being flinged and user touches, stop the fling. isFinished will be false if being flinged.
                 */
                completeScroll();

                // Remember where the motion event started
                mLastMotionX=mInitialMotionX=ev.getX();
                mActivePointerId=MotionEventCompat.getPointerId(ev, 0);
                break;
            }
            case MotionEvent.ACTION_MOVE:
                if(!mIsBeingDragged) {
                    final int pointerIndex=MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x=MotionEventCompat.getX(ev, pointerIndex);
                    final float xDiff=Math.abs(x - mLastMotionX);
                    final float y=MotionEventCompat.getY(ev, pointerIndex);
                    final float yDiff=Math.abs(y - mLastMotionY);
                    if(DEBUG)
                        Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
                    if(xDiff > mTouchSlop && xDiff > yDiff) {
                        if(DEBUG)
                            Log.v(TAG, "Starting drag!");
                        mIsBeingDragged=true;
                        mLastMotionX=x;
                        setScrollState(SCROLL_STATE_DRAGGING);
                        setScrollingCacheEnabled(true);
                    }
                }
                if(mIsBeingDragged) {
                    // Scroll to follow the motion event
                    final int activePointerIndex=MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x=MotionEventCompat.getX(ev, activePointerIndex);
                    final float deltaX=mLastMotionX - x;
                    mLastMotionX=x;
                    float oldScrollX=getScrollX();
                    float scrollX=oldScrollX + deltaX;
                    final int width=getWidth();
                    final int widthWithMargin=width + mPageMargin;

                    final int lastItemIndex=mAdapter.getCount() - 1;
                    final float leftBound=Math.max(0, (mCurItem - 1) * widthWithMargin);
                    final float rightBound=Math.min(mCurItem + 1, lastItemIndex) * widthWithMargin;
                    if(scrollX < leftBound) {
                        if(leftBound == 0) {
                            float over=-scrollX;
                            needsInvalidate=mLeftEdge.onPull(over / width);
                        }
                        scrollX=leftBound;
                    } else if(scrollX > rightBound) {
                        if(rightBound == lastItemIndex * widthWithMargin) {
                            float over=scrollX - rightBound;
                            needsInvalidate=mRightEdge.onPull(over / width);
                        }
                        scrollX=rightBound;
                    }
                    // Don't lose the rounded component
                    mLastMotionX+=scrollX - (int)scrollX;
                    scrollTo((int)scrollX, getScrollY());
                    if(mOnPageChangeListener != null) {
                        final int position=(int)scrollX / widthWithMargin;
                        final int positionOffsetPixels=(int)scrollX % widthWithMargin;
                        final float positionOffset=(float)positionOffsetPixels / widthWithMargin;
                        mOnPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if(mIsBeingDragged) {
                    final VelocityTracker velocityTracker=mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity=(int)VelocityTrackerCompat.getXVelocity(velocityTracker, mActivePointerId);
                    mPopulatePending=true;
                    final int widthWithMargin=getWidth() + mPageMargin;
                    final int scrollX=getScrollX();
                    final int currentPage=scrollX / widthWithMargin;
                    int nextPage=initialVelocity > 0 ? currentPage : currentPage + 1;
                    setCurrentItemInternal(nextPage, true, true, initialVelocity);

                    mActivePointerId=INVALID_POINTER;
                    endDrag();
                    needsInvalidate=mLeftEdge.onRelease() | mRightEdge.onRelease();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if(mIsBeingDragged) {
                    setCurrentItemInternal(mCurItem, true, true);
                    mActivePointerId=INVALID_POINTER;
                    endDrag();
                    needsInvalidate=mLeftEdge.onRelease() | mRightEdge.onRelease();
                }
                break;
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int index=MotionEventCompat.getActionIndex(ev);
                final float x=MotionEventCompat.getX(ev, index);
                mLastMotionX=x;
                mActivePointerId=MotionEventCompat.getPointerId(ev, index);
                break;
            }
            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                mLastMotionX=MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
                break;
        }
        if(needsInvalidate) {
            invalidate();
        }
        return true;
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        boolean needsInvalidate=false;

        final int overScrollMode=ViewCompat.getOverScrollMode(this);
        if(overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS
            || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && mAdapter != null && mAdapter.getCount() > 1)) {
            if(!mLeftEdge.isFinished()) {
                final int restoreCount=canvas.save();
                final int height=getHeight() - getPaddingTop() - getPaddingBottom();

                canvas.rotate(270);
                canvas.translate(-height + getPaddingTop(), 0);
                mLeftEdge.setSize(height, getWidth());
                needsInvalidate|=mLeftEdge.draw(canvas);
                canvas.restoreToCount(restoreCount);
            }
            if(!mRightEdge.isFinished()) {
                final int restoreCount=canvas.save();
                final int width=getWidth();
                final int height=getHeight() - getPaddingTop() - getPaddingBottom();
                final int itemCount=mAdapter != null ? mAdapter.getCount() : 1;

                canvas.rotate(90);
                canvas.translate(-getPaddingTop(), -itemCount * (width + mPageMargin) + mPageMargin);
                mRightEdge.setSize(height, width);
                needsInvalidate|=mRightEdge.draw(canvas);
                canvas.restoreToCount(restoreCount);
            }
        } else {
            mLeftEdge.finish();
            mRightEdge.finish();
        }

        if(needsInvalidate) {
            // Keep animating
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // Draw the margin drawable if needed.
        if(mPageMargin > 0 && mMarginDrawable != null) {
            final int scrollX=getScrollX();
            final int width=getWidth();
            final int offset=scrollX % (width + mPageMargin);
            if(offset != 0) {
                // Pages fit completely when settled; we only need to draw when
                // in between
                final int left=scrollX - offset + width;
                mMarginDrawable.setBounds(left, 0, left + mPageMargin, getHeight());
                mMarginDrawable.draw(canvas);
            }
        }
    }

    /**
     * Start a fake drag of the pager.
     * <p>
     * A fake drag can be useful if you want to synchronize the motion of the ViewPager with the touch scrolling of another view,
     * while still letting the ViewPager control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) Call
     * {@link #fakeDragBy(float)} to simulate the actual drag motion. Call {@link #endFakeDrag()} to complete the fake drag and
     * fling as necessary.
     * <p>
     * During a fake drag the ViewPager will ignore all touch events. If a real drag is already in progress, this method will return
     * false.
     * @return true if the fake drag began successfully, false if it could not be started.
     * @see #fakeDragBy(float)
     * @see #endFakeDrag()
     */
    public boolean beginFakeDrag() {
        if(mIsBeingDragged) {
            return false;
        }
        mFakeDragging=true;
        setScrollState(SCROLL_STATE_DRAGGING);
        mInitialMotionX=mLastMotionX=0;
        if(mVelocityTracker == null) {
            mVelocityTracker=VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
        final long time=SystemClock.uptimeMillis();
        final MotionEvent ev=MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
        mVelocityTracker.addMovement(ev);
        ev.recycle();
        mFakeDragBeginTime=time;
        return true;
    }

    /**
     * End a fake drag of the pager.
     * @see #beginFakeDrag()
     * @see #fakeDragBy(float)
     */
    public void endFakeDrag() {
        if(!mFakeDragging) {
            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
        }

        final VelocityTracker velocityTracker=mVelocityTracker;
        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
        int initialVelocity=(int)VelocityTrackerCompat.getYVelocity(velocityTracker, mActivePointerId);
        mPopulatePending=true;
        if((Math.abs(initialVelocity) > mMinimumVelocity) || Math.abs(mInitialMotionX - mLastMotionX) >= (getWidth() / 3)) {
            if(mLastMotionX > mInitialMotionX) {
                setCurrentItemInternal(mCurItem - 1, true, true);
            } else {
                setCurrentItemInternal(mCurItem + 1, true, true);
            }
        } else {
            setCurrentItemInternal(mCurItem, true, true);
        }
        endDrag();

        mFakeDragging=false;
    }

    /**
     * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
     * @param xOffset Offset in pixels to drag by.
     * @see #beginFakeDrag()
     * @see #endFakeDrag()
     */
    public void fakeDragBy(float xOffset) {
        if(!mFakeDragging) {
            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
        }

        mLastMotionX+=xOffset;
        float scrollX=getScrollX() - xOffset;
        final int width=getWidth();
        final int widthWithMargin=width + mPageMargin;

        final float leftBound=Math.max(0, (mCurItem - 1) * widthWithMargin);
        final float rightBound=Math.min(mCurItem + 1, mAdapter.getCount() - 1) * widthWithMargin;
        if(scrollX < leftBound) {
            scrollX=leftBound;
        } else if(scrollX > rightBound) {
            scrollX=rightBound;
        }
        // Don't lose the rounded component
        mLastMotionX+=scrollX - (int)scrollX;
        scrollTo((int)scrollX, getScrollY());
        if(mOnPageChangeListener != null) {
            final int position=(int)scrollX / widthWithMargin;
            final int positionOffsetPixels=(int)scrollX % widthWithMargin;
            final float positionOffset=(float)positionOffsetPixels / widthWithMargin;
            mOnPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
        }

        // Synthesize an event for the VelocityTracker.
        final long time=SystemClock.uptimeMillis();
        final MotionEvent ev=MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, mLastMotionX, 0, 0);
        mVelocityTracker.addMovement(ev);
        ev.recycle();
    }

    /**
     * Returns true if a fake drag is in progress.
     * @return true if currently in a fake drag, false otherwise.
     * @see #beginFakeDrag()
     * @see #fakeDragBy(float)
     * @see #endFakeDrag()
     */
    public boolean isFakeDragging() {
        return mFakeDragging;
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex=MotionEventCompat.getActionIndex(ev);
        final int pointerId=MotionEventCompat.getPointerId(ev, pointerIndex);
        if(pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex=pointerIndex == 0 ? 1 : 0;
            mLastMotionX=MotionEventCompat.getX(ev, newPointerIndex);
            mActivePointerId=MotionEventCompat.getPointerId(ev, newPointerIndex);
            if(mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }

    private void endDrag() {
        mIsBeingDragged=false;
        mIsUnableToDrag=false;

        if(mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker=null;
        }
    }

    private void setScrollingCacheEnabled(boolean enabled) {
        if(mScrollingCacheEnabled != enabled) {
            mScrollingCacheEnabled=enabled;
            if(USE_CACHE) {
                final int size=getChildCount();
                for(int i=0; i < size; ++i) {
                    final View child=getChildAt(i);
                    if(child.getVisibility() != GONE) {
                        child.setDrawingCacheEnabled(enabled);
                    }
                }
            }
        }
    }

    /**
     * Tests scrollability within child views of v given a delta of dx.
     * @param v View to test for horizontal scrollability
     * @param checkV Whether the view v passed should itself be checked for scrollability (true), or just its children (false).
     * @param dx Delta scrolled in pixels
     * @param x X coordinate of the active touch point
     * @param y Y coordinate of the active touch point
     * @return true if child views of v can be scrolled by delta of dx.
     */
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if(v instanceof ViewGroup) {
            final ViewGroup group=(ViewGroup)v;
            final int scrollX=v.getScrollX();
            final int scrollY=v.getScrollY();
            final int count=group.getChildCount();
            // Count backwards - let topmost views consume scroll distance
            // first.
            for(int i=count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child=group.getChildAt(i);
                if(x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop()
                    && y + scrollY < child.getBottom()
                    && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        // Let the focused view and/or our descendants get the key first
        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
    }

    /**
     * You can call this function yourself to have the scroll view perform scrolling from a key event, just as if the event had been
     * dispatched to it by the view hierarchy.
     * @param event The key event to execute.
     * @return Return true if the event was handled, else false.
     */
    public boolean executeKeyEvent(KeyEvent event) {
        boolean handled=false;
        if(event.getAction() == KeyEvent.ACTION_DOWN) {
            switch(event.getKeyCode()) {
                case KeyEvent.KEYCODE_DPAD_LEFT:
                    handled=arrowScroll(FOCUS_LEFT);
                    break;
                case KeyEvent.KEYCODE_DPAD_RIGHT:
                    handled=arrowScroll(FOCUS_RIGHT);
                    break;
                case KeyEvent.KEYCODE_TAB:
                    if(KeyEventCompat.hasNoModifiers(event)) {
                        handled=arrowScroll(FOCUS_FORWARD);
                    } else if(KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
                        handled=arrowScroll(FOCUS_BACKWARD);
                    }
                    break;
            }
        }
        return handled;
    }

    public boolean arrowScroll(int direction) {
        View currentFocused=findFocus();
        if(currentFocused == this)
            currentFocused=null;

        boolean handled=false;

        View nextFocused=FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
        if(nextFocused != null && nextFocused != currentFocused) {
            if(direction == View.FOCUS_LEFT) {
                // If there is nothing to the left, or this is causing us to
                // jump to the right, then what we really want to do is page
                // left.
                if(currentFocused != null && nextFocused.getLeft() >= currentFocused.getLeft()) {
                    handled=pageLeft();
                } else {
                    handled=nextFocused.requestFocus();
                }
            } else if(direction == View.FOCUS_RIGHT) {
                // If there is nothing to the right, or this is causing us to
                // jump to the left, then what we really want to do is page
                // right.
                if(currentFocused != null && nextFocused.getLeft() <= currentFocused.getLeft()) {
                    handled=pageRight();
                } else {
                    handled=nextFocused.requestFocus();
                }
            }
        } else if(direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
            // Trying to move left and nothing there; try to page.
            handled=pageLeft();
        } else if(direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
            // Trying to move right and nothing there; try to page.
            handled=pageRight();
        }
        if(handled) {
            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
        }
        return handled;
    }

    boolean pageLeft() {
        if(mCurItem > 0) {
            setCurrentItem(mCurItem - 1, true);
            return true;
        }
        return false;
    }

    boolean pageRight() {
        if(mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {
            setCurrentItem(mCurItem + 1, true);
            return true;
        }
        return false;
    }

    /**
     * We only want the current page that is being shown to be focusable.
     */
    @Override
    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
        final int focusableCount=views.size();

        final int descendantFocusability=getDescendantFocusability();

        if(descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
            for(int i=0; i < getChildCount(); i++) {
                final View child=getChildAt(i);
                if(child.getVisibility() == VISIBLE) {
                    ItemInfo ii=infoForChild(child);
                    if(ii != null && ii.position == mCurItem) {
                        child.addFocusables(views, direction, focusableMode);
                    }
                }
            }
        }

        // we add ourselves (if focusable) in all cases except for when we are
        // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.
        // this is
        // to avoid the focus search finding layouts when a more precise search
        // among the focusable children would be more interesting.
        if(descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
        // No focusable descendants
            (focusableCount == views.size())) {
            // Note that we can't call the superclass here, because it will
            // add all views in. So we need to do the same thing View does.
            if(!isFocusable()) {
                return;
            }
            if((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && isInTouchMode() && !isFocusableInTouchMode()) {
                return;
            }
            if(views != null) {
                views.add(this);
            }
        }
    }

    /**
     * We only want the current page that is being shown to be touchable.
     */
    @Override
    public void addTouchables(ArrayList<View> views) {
        // Note that we don't call super.addTouchables(), which means that
        // we don't call View.addTouchables(). This is okay because a ViewPager
        // is itself not touchable.
        for(int i=0; i < getChildCount(); i++) {
            final View child=getChildAt(i);
            if(child.getVisibility() == VISIBLE) {
                ItemInfo ii=infoForChild(child);
                if(ii != null && ii.position == mCurItem) {
                    child.addTouchables(views);
                }
            }
        }
    }

    /**
     * We only want the current page that is being shown to be focusable.
     */
    @Override
    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
        int index;
        int increment;
        int end;
        int count=getChildCount();
        if((direction & FOCUS_FORWARD) != 0) {
            index=0;
            increment=1;
            end=count;
        } else {
            index=count - 1;
            increment=-1;
            end=-1;
        }
        for(int i=index; i != end; i+=increment) {
            View child=getChildAt(i);
            if(child.getVisibility() == VISIBLE) {
                ItemInfo ii=infoForChild(child);
                if(ii != null && ii.position == mCurItem) {
                    if(child.requestFocus(direction, previouslyFocusedRect)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    @Override
    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
        // ViewPagers should only report accessibility info for the current
        // page,
        // otherwise things get very confusing.

        // TODO: Should this note something about the paging container?

        final int childCount=getChildCount();
        for(int i=0; i < childCount; i++) {
            final View child=getChildAt(i);
            if(child.getVisibility() == VISIBLE) {
                final ItemInfo ii=infoForChild(child);
                if(ii != null && ii.position == mCurItem && child.dispatchPopulateAccessibilityEvent(event)) {
                    return true;
                }
            }
        }

        return false;
    }

    private class PagerObserver extends DataSetObserver {

        @Override
        public void onChanged() {
            dataSetChanged();
        }

        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }
}